"""
Event emitter interfaces and implementations for the training event system.

This module provides the abstraction layer for event emission in the continual
learning framework. Event emitters are responsible for creating event contexts
and dispatching them to registered handlers through the event manager.

The module implements several emitter types:
- EventEmitter: Abstract base class defining the emitter interface
- NullEventEmitter: No-op implementation for when events are disabled
- DefaultEventEmitter: Standard implementation using EventManager
- PerformanceAwareEventEmitter: Optimized version with emission throttling

Design Patterns:
- Abstract Factory: EventEmitter defines the interface
- Null Object: NullEventEmitter provides safe no-op behavior
- Decorator/Proxy: PerformanceAwareEventEmitter adds throttling behavior

Example:
    >>> from learners.events.emitters import DefaultEventEmitter
    >>> from learners.events.manager import EventManager
    >>> from learners.events.types import EventType
    >>>
    >>> manager = EventManager()
    >>> emitter = DefaultEventEmitter(manager, learner_instance)
    >>>
    >>> # Emit events
    >>> emitter.emit_event(EventType.ON_TRAIN_START)
    >>> emitter.emit_event(EventType.ON_EPOCH_START, epoch=1)

Architecture:
    Event emitters act as the bridge between training code and the event
    management system. They encapsulate the logic for creating proper event
    contexts and enriching them with learner-specific information before
    dispatching to handlers.
"""

from typing import Any, Set, Optional
import logging
import weakref

from learners.interfaces.event import (
    EventManager,
    EventType,
    EventContext,
    EventEmitter,
)


class NullEventEmitter(EventEmitter):
    """
    Null object pattern implementation that does nothing when events are disabled.

    This emitter is used when event processing is disabled, providing a
    safe no-op implementation that allows event emission calls to remain
    in the code without any performance impact.
    """

    def emit_event(self, event: EventType, **kwargs) -> None:
        """Do nothing - events are disabled."""
        pass

    def register_handler(self, event: EventType, handler: Any) -> None:
        """Do nothing - events are disabled."""
        pass

    def unregister_handler(self, event: EventType, handler: Any) -> None:
        """Do nothing - events are disabled."""
        pass


class DefaultEventEmitter(EventEmitter):
    """
    Default event emitter implementation using an EventManager.

    This emitter creates EventContext objects and dispatches them through
    the provided EventManager. It enriches events with learner-specific
    context data and handles errors gracefully.
    """

    def __init__(self, event_manager: EventManager, learner_instance: Any):
        """
        Initialize the emitter with an event manager and learner instance.

        Args:
            event_manager: The EventManager to dispatch events through
            learner_instance: The learner instance that will be the source of events
        """
        self._event_manager = event_manager
        self._learner = learner_instance
        self._logger = logging.getLogger(self.__class__.__name__)

    def emit_event(self, event: EventType, **kwargs) -> None:
        """
        Emit an event through the event manager.

        Creates an EventContext with the learner instance and additional
        context data, enriches it with learner-specific information,
        and dispatches it through the event manager.
        """
        try:
            # Create context with learner instance and additional data
            context = EventContext(event=event, learner=self._learner, **kwargs)

            # Add learner-specific context data
            self._enrich_context(context)

            # Emit the event through the manager
            self._event_manager.emit(context)

        except Exception as e:
            self._logger.error(f"Error emitting event {event}: {e}")

    def register_handler(self, event: EventType, handler: Any) -> None:
        """Register an event handler through the event manager."""
        self._event_manager.register(event, handler)

    def unregister_handler(self, event: EventType, handler: Any) -> None:
        """Unregister an event handler through the event manager."""
        self._event_manager.unregister(event, handler)

    def _enrich_context(self, context: EventContext) -> None:
        """
        Enrich context with learner-specific data.

        Automatically extracts common attributes from the learner instance
        and adds them to the event context for use by handlers.
        """
        learner = self._learner

        # Add common learner attributes if available
        if hasattr(learner, "_cur_task"):
            context.task_id = learner._cur_task
        if hasattr(learner, "_cur_epoch"):
            context.epoch = learner._cur_epoch
        if hasattr(learner, "_known_classes"):
            context.set("known_classes", learner._known_classes)
        if hasattr(learner, "_network"):
            context.set("network", learner._network)
        if hasattr(learner, "_old_network"):
            context.set("old_network", learner._old_network)


# LazyLearnerProxy and ProxyEventContext classes are no longer needed
# as their functionality has been integrated directly into EventContext


class PerformanceAwareEventEmitter(EventEmitter):
    """
    Performance-optimized event emitter that reduces communication overhead.

    This emitter is designed to address performance concerns when passing large
    learner objects in event contexts. It features:

    1. Lazy attribute access - only loads attributes when actually needed
    2. Uses weak references to reduce memory pressure
    3. Attribute access monitoring - tracks which attributes are actually used
    4. Configurable attribute blocking - prevents access to very large attributes

    This implementation significantly reduces communication overhead in
    distributed training scenarios or when learners contain large data structures
    while remaining flexible enough to support different event handlers with
    varying data needs.
    """

    def __init__(
        self,
        event_manager: EventManager,
        learner_instance: Any,
        skip_events: Optional[Set[EventType]] = None,
        blocked_attrs: Optional[Set[str]] = None,
        monitored_attrs: Optional[Set[str]] = None
    ):
        """
        Initialize the optimized emitter.

        Args:
            event_manager: The EventManager to dispatch events through
            learner_instance: The learner instance to extract data from
            skip_events: Set of event types to completely ignore
            blocked_attrs: Set of attribute names that should not be accessed directly
            monitored_attrs: Set of attribute names to log when accessed
        """
        self._event_manager = event_manager
        # Use weak reference to avoid circular references
        self._learner_ref = weakref.ref(learner_instance)
        self._logger = logging.getLogger(self.__class__.__name__)

        # Events to skip entirely (e.g., very high frequency events)
        self._skip_events = skip_events or set()

        # Configuration for lazy loading
        self._blocked_attrs = blocked_attrs
        self._monitored_attrs = monitored_attrs

    @property
    def _learner(self):
        """Safely access the learner through weak reference"""
        learner = self._learner_ref()
        if learner is None:
            raise RuntimeError("Learner instance has been garbage collected")
        return learner

    def emit_event(self, event: EventType, **kwargs) -> None:
        """
        Emit an event with optimized data transfer.

        Instead of passing the entire learner instance, this method:
        1. Uses EventContext's built-in lazy loading capability
        2. Completely skips events in the skip_events set

        Args:
            event: Type of event to emit
            **kwargs: Additional event data
        """
        # Skip ignored events entirely
        if event in self._skip_events:
            return

        try:
            # Extract common attributes directly for easy access
            learner = self._learner
            extra_kwargs = kwargs.copy()

            if hasattr(learner, "_cur_task") and "task_id" not in extra_kwargs:
                extra_kwargs["task_id"] = learner._cur_task
            if hasattr(learner, "_cur_epoch") and "epoch" not in extra_kwargs:
                extra_kwargs["epoch"] = learner._cur_epoch

            # Create optimized context with lazy loading using the factory method
            context = EventContext.create_optimized(
                event=event,
                learner_ref=self._learner_ref,
                blocked_attrs=self._blocked_attrs,
                monitored_attrs=self._monitored_attrs,
                **extra_kwargs
            )

            # Add learner class name for type checking
            context.set("learner_class", learner.__class__.__name__)

            # Add additional context data if needed
            if hasattr(learner, "_known_classes"):
                context.set("known_classes", learner._known_classes)

            # Emit the optimized event
            self._event_manager.emit(context)

        except Exception as e:
            self._logger.error(f"Error emitting optimized event {event}: {e}")

            # Log more details in debug mode
            self._logger.debug(f"Error details: {e}", exc_info=True)

    def register_handler(self, event: EventType, handler: Any) -> None:
        """Register an event handler through the event manager."""
        self._event_manager.register(event, handler)

    def unregister_handler(self, event: EventType, handler: Any) -> None:
        """Unregister an event handler through the event manager."""
        self._event_manager.unregister(event, handler)
